package tink.macro;
import haxe.macro.Expr;
@:macro class Magic {
	/**
	 * Will "ASTify" an expression. 
	 * @param	e
	 * @return
	 * 
	 * @usage
	 * This is quite a magic macro. What it generates is an expression tree,
	 * that evaluates to the expression passed to the function (don't know how to explain this better).
	 * Simple Example:
	 * 
	 * 	var e = Magic.AST(value);
	 * 
	 * Will become:
	 * 
	 * 	var e = { expr: EConst(CIdent('value')), pos: Context.getCurrentPos() } 
	 * 
	 * Now this alone is not so helpful, but the macro allows injecting values from the scope it is called in.
	 * 
	 * Identifier Injection
	 * ====================
	 * 
	 * Identifier injection is done by prefixing the desired identifier to be injected with 'eval__'.
	 * 
	 * 	var e = Magic.AST(o.eval__field);
	 * 
	 * Will thus become:
	 * 
	 * 	var e = EField(EConst(CIdent('o')), field);//the `Expr` overhead has been omitted for readability here
	 * 
	 * Expression Injection
	 * ====================
	 * 
	 * Expression injection is intended to inject a whole subexpression. Expressions you want injected need to be enclosed in a eval-"call".
	 * 
	 * Example (combined with identifier injection):
	 * 
	 * 	@:macro class MacroReflect {
	 * 		public static function field(target:Expr, field:String) {//requires haXe 2.08
	 * 			return Magic.AST(eval(target).eval_field);
	 * 		}
	 * 	}
	 * 	
	 * You would thus have:
	 * 
	 * 	MacroReflect.field(o, 'foo')
	 * 
	 * becoming:
	 * 
	 * 	o.foo
	 */
	public static function AST(e) {
		return new ASTBuilder().transform(e);
	}
	public static function or(e1, e2) {
		return new OrTransformer().transform(e1, e2);
	}
	public static function render(s:String) {
		return new Substitutor().transform(s);
	}
}
#if macro
	import tink.util.Printer;
	import haxe.macro.Context;
	import haxe.macro.Expr;
	private class ASTBuilder extends Helper {
		var here:Expr;
		public function new() {
			this.here = this.call('haxe.macro.Context.currentPos', []);
		}
		function isEval(value:ExprDef) {
			return 
				Type.enumConstructor(value) == 'ECall' && 
				Type.enumEq(EConst(CIdent('eval')), Type.enumParameters(value)[0].expr);
		}
		function transformEnum(value:Dynamic) {
			var params = Lambda.array(Lambda.map(Type.enumParameters(value), transform));
			var constr = ident(Type.enumConstructor(value));
			return
				if (params.length > 0) 
					expr(ECall(constr, params));
				else
					constr;			
		}
		public function transform(value:Dynamic):Expr {
			return
				switch (Type.typeof(value)) {
					case TNull:
						ident('null');
					case TInt, TFloat:
						throw "where the hell did you get numerical values?";
					case TBool:
						ident(value);
					case TObject:
						try {
							var fields = [];
							for (field in Reflect.fields(value)) 
								fields.push({ 
									field:field, 
									expr: transform(Reflect.field(value, field)), 
								});
							expr(EObjectDecl(fields));							
						}
						catch (e:Evaled) {
							e.expr;
						}
					case TClass(c):
						if (c == String) 
							string(value);
						else if (c == Array) {
							var value:Array<Dynamic> = value;
							var ret = [];
							for (entry in value)
								ret.push(transform(entry));
							expr(EArrayDecl(ret));
						}
						else
							throw "Cannot transform " + Type.getClassName(c);
					case TEnum(e):
						if (e == ExprDef) {
							var value = cast(value, ExprDef);
							var ret = 
								switch (value) {
									case ECall(e, args):
										switch (e.expr) {
											case EConst(c):
												switch (c) {
													case CIdent(ident):
														if (ident == 'eval') {
															if (args.length != 1) 
																error('eval requires one argument', e.pos);
															else
																throw new Evaled(args[0]);//Don't do this at home!!!
														}
														else 
															null;
													default: null;
												}
											default: null;
										}
									case EVars(vars):
										var ret = [];
										for (v in vars) {
											if (StringTools.startsWith(v.name, 'eval__')) {
												ret.push(expr(
													EObjectDecl([
														{ field: 'name', expr: ident(v.name.substr(6)) },
														{ field: 'expr', expr: transform(v.expr) },
														{ field: 'type', expr: transform(v.type) }
													])
												));
											}
											else 
												ret.push(transform(v));
										}
										expr(
											ECall(
												ident('EVars'), 
												[expr(EArrayDecl(ret))]
											)
										);
									case EField(e, field), EType(e, field):
										if (StringTools.startsWith(field, 'eval__')) 
											makeEnum(
												Type.enumConstructor(value), 
												[transform(e), ident(field.substr(6))]
											);
										else 
											null;
									case EConst(c):
										switch (c) {
											case CIdent(id):
												//FIXME: this currently doesn't work as it should.
												if (StringTools.startsWith(id, 'eval__')) 
													throw new Evaled(
														transform(expr(EConst(CIdent(id.substr(6)))))
														//makeEnum(
															//'EConst', 
															//[makeEnum(
																//'CIdent', 
																//[ident(id.substr(6))]
															//)]
														//)
													);
												else 
													null;
											default:
												null;
										}
									default: 
										null;
								}
							if (ret == null) 
								transformEnum(value);
							else
								ret;
						}
						else transformEnum(value);
					default:
						if (StringTools.startsWith(Std.string(value), '#pos')) 
							here;
						else
							throw "Can't transform " + Std.string(value);
				}
		}	
	}
	private class Evaled {
		public var expr(default, null):Expr;
		public function new(expr) {
			this.expr = expr;
		}
	}
	private class OrTransformer extends Helper {
		public function new();
		
		public function transform(e1:Expr, e2:Expr):Expr {
			var id = 'tmp_' + Context.signature(e1) + Context.signature(e2);
			return logExpr(Magic.AST( {
				var eval__id = eval(e1);
				if (eval(ident(id)) == null)
					eval(e2);
				else
					eval(ident(id));
			}));
		}
	}
	private class Substitutor extends Helper {
		public function new();
		public function transform(s:String) {
			var chunks = s.split('{');
			var head = string(chunks.shift());
			var parts = [];
			for (chunk in chunks) {
				var count = 1, char, len = 0;
				for (i in 0...chunk.length) {
					switch (char = chunk.charCodeAt(i)) {
						case 125:// '}'
							if (--count == 0) break;
						case 123:// '{'
							count++;
					}
					len++;
				}
				if (count != 0) 
					error('failed to parse string because of misleading curly braces');
				parts.push(Context.parse(chunk.substr(0, len), Context.currentPos()));
				parts.push(string(chunk.substr(len + 1)));
			}
			for (part in parts) 
				head = Magic.AST(eval(head) + eval(part));
			return head;
		}
	}
#end